#include "filereader.h"
#include "command.h"
#include "aperturetemplate.h"

#include <QDebug>
#include <QRegularExpression>
#include <QtMath>

#include <cctype>

namespace LeGerber
{

	namespace ParserPrivate
	{
		// [-+]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?
		static const QString DECIMAL("[\\+-]?\\d*[.]?\\d*([eE][\\+-]?\\d+)?");
		static const QString NAME("[a-zA-Z_\\.$][a-zA-Z_\\.$0-9]*");
		static const QString NAME_RELAXED("[a-zA-Z_\\.$][a-zA-Z_\\.$0-9 \\-~/]*"); // w/ ' ', '-', '~' and '/'
		static const QString FIELD("[a-zA-Z0-9_+\\-/!?<>”’(){}.\\|&@# ;$:=]+"); // STRING w/o ','
		static const QString FIELD_RELAXED("[a-zA-Z0-9_+\\-/!?<>”’(){}.\\|&@# ;$:=~]+"); // w/ ~
		static const QString APERTURENUMBER("[1-9][0-9]+");
	}

	FileReader::FileReader(QObject *parent) : QObject(parent)
	{
		clearErrorAndWarnings();
		clearCommands();
		clearContext();
		clearStatisitics();
	}

	FileReader::~FileReader()
	{
		clearCommands();
	}

	void FileReader::setData(const QByteArray &data)
	{
		m_data = data;

		clearErrorAndWarnings();
		clearCommands();
		clearContext();
		clearStatements();
	}

	// Legacy token: [^%*\r\n]*\*
	// Extended token: %[^%]*%
	// Separator: [\r\n]*
	bool FileReader::parse()
	{
		if (m_data.isEmpty())
			return true;

		enum
		{
			WaitingForStart,
			WaitingForEndOfLegacy,
			WaitingForEndOfExtended,
		};

		int state = WaitingForStart;
		int start = 0;
		int length = 0;
		int i = 0;
		while ( i < m_data.length())
		{
			char c = m_data.at(i);
			switch (state)
			{
				case WaitingForStart:
				{
					switch (c)
					{
						case '%':
						{
							start = i;
							length = 1;
							state = WaitingForEndOfExtended;
							break;
						}
						case '*':
						case '\r':
						case '\n':
						{
							length++;
							break;
						}
						default:
						{
							bool isSpace = false;
							if (length <= 4)
							{
								for (int i=0; i<length; i++)
									isSpace = isSpace || std::isspace(m_data.at(start+i));
							}
							if (length != 0 && !isSpace)
							{
								if (!handleGarbage(start, length))
									return false;
							}
							start = i;
							length = 1;
							state = WaitingForEndOfLegacy;
							break;
						}
					}
					break;
				}
				case WaitingForEndOfLegacy:
				{
					switch (c)
					{
						case '*':
						{
							length++;
							if (handleLegacyStatement(start, length))
								m_statements.append(Statement(start, length));
							else if (!handleGarbage(start, length))
								return false;
							start += length;
							length = 0;
							state = WaitingForStart;
							break;
						}
						case '%':
						{
							length++;
							state = WaitingForStart;
							break;
						}
						default:
						{
							length++;
							break;
						}
					}
					break;
				}
				case WaitingForEndOfExtended:
				{
					switch (c)
					{
						case '%':
						{
							length++;
							if (handleExtentedStatement(start, length))
								m_statements.append(Statement(start, length));
							else if (!handleGarbage(start, length))
								return false;
							start += length;
							length = 0;
							state = WaitingForStart;
							break;
						}
						default:
						{
							length++;
							break;
						}
					}
					break;
				}
			}
			i++;
		}

		return true;
	}

	bool FileReader::parseXYIJ(int start, int length, QPointF &xy, QPointF &ij)
	{
		//qDebug() << m_data.mid(start, length);

		xy.setX(qSNaN());
		xy.setY(qSNaN());
		ij.setX(qSNaN());
		ij.setY(qSNaN());

		if (length == 0) // Empty is OK
			return true;

		if (length < 2) // X,Y,I,J and one digit
		{
			setError(start, "Malformed XYIJ code (length < 2)");
			return false;
		}

		int i = length -1;
		qint64 value = 0;
		int weight = 1;
		enum
		{
			WaitingForNumber,
			WaitingForLetter
		};
		int state = WaitingForNumber;
		while (i >= 0)
		{
			char c0 = m_data.at(start + i);
			switch (state)
			{
				case WaitingForNumber:
				{
					if (c0 >= '0' && c0 <= '9')
					{
						value += int(c0 - '0') * weight;
						weight *= 10;
						i--;
					}
					else if (c0 == '-' || c0 == '+')
					{
						if (weight == 1)
						{
							setError(start, "Malformed XYIJ code (sign)");
							return false;
						}
						if (c0 == '-')
							value *= -1;
						state = WaitingForLetter;
						i--;
					}
					else
					{
						state = WaitingForLetter;
					}
					break;
				}
				case WaitingForLetter:
				{
					switch (c0)
					{
						case 'X':
							xy.setX(value * m_context.coordinateDivider);
							weight = 1;
							value = 0;
							state = WaitingForNumber;
							break;
						case 'Y':
							xy.setY(value * m_context.coordinateDivider);
							weight = 1;
							value = 0;
							state = WaitingForNumber;
							break;
						case 'I':
							ij.setX(value * m_context.coordinateDivider);
							weight = 1;
							value = 0;
							state = WaitingForNumber;
							break;
						case 'J':
							ij.setY(value * m_context.coordinateDivider);
							weight = 1;
							value = 0;
							state = WaitingForNumber;
							break;
						default:
							return false;
					}
					i--;
				}
			}
		}
		if (state == WaitingForNumber && weight == 1)
			return true;
		return false;
	}

	bool FileReader::handleLegacyStatement(int start, int length)
	{
		if (length < 3)
		{
			setError(start, "Malformed legacy statement (length < 3)");
			return false;
		}

		if (length == 3)
		{
			// Only standalone coordinate: X0*
			QPointF xy;
			QPointF ij;
			if (!parseXYIJ(start, length - 1, xy, ij))
			{
				return false;
			}
			auto cmd = new InterpolateCommand(xy.x(), xy.y(), ij.x(), ij.y());
			m_commands.append(cmd);
			m_codeStats.D01++;
			return true;
		}

		// From here, we expect at least 4 char: [GDM]XX*
		char c0 = m_data.at(start + 0);
		char c1 = m_data.at(start + 1);
		char c2 = m_data.at(start + 2);
		char cn1 = m_data.at(start + length - 2);
		char cn2 = m_data.at(start + length - 3);
		char cn3 = m_data.at(start + length - 4);

		// ca. 80% of statements are D-codes that ends with D0[123]\* and are not prefixed by G-code
		if (cn3 == 'D' && cn2 == '0' && cn1 >= '1' && cn1 <= '3' && c0 != 'G')
		{
			switch (cn1)
			{
				case '1':
				{
					QPointF xy;
					QPointF ij;
					if (!parseXYIJ(start, length - 4, xy, ij))
					{
						addWarning(start, "Could not parse XYIJ/D01");
						return false;
					}
					auto cmd = new InterpolateCommand(xy.x(), xy.y(), ij.x(), ij.y());
					m_commands.append(cmd);
					m_codeStats.D01++;
					return true;
				}
				case '2':
				{
					QPointF xy;
					QPointF ij;
					if (!parseXYIJ(start, length - 4, xy, ij))
					{
						addWarning(start, "Could not parse XYIJ/D02");
						return false;
					}
					auto cmd = new MoveCommand(xy.x(),xy.y());
					m_commands.append(cmd);
					m_codeStats.D02++;
					return true;
				}
				case '3':
				{
					QPointF xy;
					QPointF ij;
					if (!parseXYIJ(start, length - 4, xy, ij))
					{
						addWarning(start, "Could not parse XYIJ/D03");
						return false;
					}
					auto cmd = new FlashCommand(xy.x(),xy.y());
					m_commands.append(cmd);
					m_codeStats.D03++;
					return true;
				}
			}
		}
		// ca. 18% of statements are short G-codes (^G[0-9][0-9]*$)
		if (length == 4 && cn3 == 'G')
		{
			switch (cn2)
			{
				case '3':
					// >99% of short G-codes are G36/37
					switch (cn1)
					{
						case '6':
							m_commands.append(new BeginRegionCommand());
							m_codeStats.G36++;
							return true;
						case '7':
							m_commands.append(new EndRegionCommand());
							m_codeStats.G37++;
							return true;
					}
					break;
				case '0':
					// G0[1234]
					switch (cn1)
					{
						case '1':
							m_commands.append(new SelectInterpolationCommand(LinearInterpolation));
							m_codeStats.G01++;
							return true;
						case '2':
							m_commands.append(new SelectInterpolationCommand(ClockWiseInterpolation));
							m_codeStats.G02++;
							return true;
						case '3':
							m_commands.append(new SelectInterpolationCommand(CounterClockWiseInterpolation));
							m_codeStats.G03++;
							return true;
						case '4':
							m_commands.append(new CommentCommand(QStringLiteral("G04"), QString()));
							return true;
					}
					break;
				case '7':
					// G7[0145]
					switch (cn1)
					{
						case '0':
							m_commands.append(new SelectUnitCommand(ImperialUnit));
							m_codeStats.G70++;
							return true;
						case '1':
							m_commands.append(new SelectUnitCommand(MetricUnit));
							m_codeStats.G71++;
							return true;
						case '4':
							m_commands.append(new SelectQuadrantCommand(SingleQuadrant));
							m_codeStats.G74++;
							return true;
						case '5':
							m_commands.append(new SelectQuadrantCommand(MultipleQuadrant));
							m_codeStats.G75++;
							return true;
					}
					break;
				case '9':
					// G9[01]
					switch (cn1)
					{
						case '0':
							m_commands.append(new SelectCoordinateNotationCommand(AbsoluteCoordinateNotation));
							m_codeStats.G90++;
							return true;
						case '1':
							m_commands.append(new SelectCoordinateNotationCommand(IncrementalCoordinateNotation));
							m_codeStats.G91++;
							return true;
					}
					break;
			}
		}
		// Current Aperture D[1-9][0-9]+\* w/ nn < 2^32 - 1 (10 digits max)
		if (length <= (1 + 10 + 1) && c0 == 'D')
		{
			bool ok;
			quint64 number = m_data.mid(start + 1,
			                            length - 1 - 1).toULong(&ok);
			if (ok && number >= 10ul && number <=  2147483647ul)
			{
				m_commands.append(new SelectApertureCommand(quint32(number)));
				m_codeStats.Dnn++;
				return true;
			}
		}
#if 1
		// Prefixed Current Aperture G54D[1-9][0-9]+\*
		if (length >= 7 && length <= (3 + 1 + 10 + 1) && c0 == 'G' && c1 =='5' && c2 == '4')
		{
			bool ok;
			quint64 number = m_data.mid(start + 3 + 1,
			                            length - 1 - 1 - 3).toUInt(&ok);
			if (ok && number >= 10ul && number <=  2147483647ul)
			{
				m_commands.append(new NullCommand()); // For G54
				m_commands.append(new SelectApertureCommand(quint32(number)));
				m_codeStats.G54++;
				m_codeStats.Dnn++;
				return true;
			}
		}
#else
		if (length >= 7 && c0 == 'G' && c1 =='5' && c2 == '4')
		{
			return handleLegacyStatement(start + 3, length - 3);
		}

#endif
		// Comments G04[^%*]*\*
		if (c0 == 'G' && c1 == '0' && c2 == '4')
		{
			m_commands.append(new CommentCommand(m_data.mid(start, 3),
			                                     m_data.mid(start + 4, length - 4)));
			m_codeStats.G04++;
			return true;
		}
		// M-codes: M0[012]\*
		if (length == 4 && c0 == 'M' && c1 == '0')
		{
			switch (c2)
			{
				case '0':
					m_commands.append(new EndCommand());
					m_codeStats.M00++;
					return true;
				case '1':
					m_commands.append(new NullCommand());
					m_codeStats.M01++;
					return true;
				case '2':
					m_commands.append(new EndCommand());
					m_codeStats.M02++;
					return true;
			}
		}
		// G+D Composition G0[123]<>D0[12]\*
		if (length >= 7 && c0 == 'G' && c1 == '0' && (c2 == '1' || c2 == '2' || c2 == '3') &&
		        cn3 == 'D' && cn2 == '0' && (cn1 == '1' || cn1 == '2'))
		{
			// G0[123]
			switch (c2)
			{
				case '1':
					m_commands.append(new SelectInterpolationCommand(LinearInterpolation));
					m_codeStats.G01++;
					break;
				case '2':
					m_commands.append(new SelectInterpolationCommand(ClockWiseInterpolation));
					m_codeStats.G02++;
					break;
				case '3':
					m_commands.append(new SelectInterpolationCommand(CounterClockWiseInterpolation));
					m_codeStats.G03++;
					break;
				default:
					return false;
			}
			return handleLegacyStatement(start + 3, length - 3);
		}
		// G55<>D03\*

		// G + coord (non-standard: G02X1830Y3076I-37J31*)

		// Standalone coordinates
		if (c0 == 'X' || c0 == 'Y')
		{
			QPointF xy;
			QPointF ij;
			if (!parseXYIJ(start, length - 1, xy, ij))
			{
				addWarning(start, "Could not parse standalone XYIJ");
				return false;
			}
			auto cmd = new InterpolateCommand(xy.x(), xy.y(), ij.x(), ij.y());
			m_commands.append(cmd);
			m_codeStats.D01++; // FIXME: needs to ensure it has been preceded by D01 (see p. 172)
			return true;
		}
		return false;
	}


	// 30894  'AM'
	//  4296  'LN'
	//  1789  'IP'
	//  1166  'IN'
	//  1089  'OF'
	//   877  'SF'
	//   117  'AS'
	//    61  'SR'
	//    14  'MI'
	//    11  'IR'

	bool FileReader::handleExtentedStatement(int start, int length)
	{
		if (length < 4)
			return false;

		QString code(m_data.mid(start + 1, 2));
		QString data(m_data.mid(start + 1 + 2, length - 1 - 2 - 1));

		data.remove(QChar('\r'));
		data.remove(QChar('\n'));

		// Non standard FSAX: (missing 'L' for Leading or 'T' for Trailing)
		// Seems to be generated by Altium (AD14), coordinates area always the right length (no need for padding of trailing/leading zeros)
		if (code == "FS") // TODO: handle (L|T)(A|I) (at least T vs L vs missing)
		{
			//static const QRegularExpression re(QString("^LAX(?<ix>[0-5])(?<fx>[4-6])Y(?<iy>[0-5])(?<fy>[4-6])\\*$"));
			//static const QRegularExpression re(QString("^LAX(?<ix>[0-9])(?<fx>[0-9])Y(?<iy>[0-9])(?<fy>[0-9])\\*$"));
			static const QRegularExpression re(QString("^(?<lt>(L|T))?(?<ai>(A|I))?(?<spec>X(?<ix>[0-9])(?<fx>[0-9])Y(?<iy>[0-9])(?<fy>[0-9]))\\*$"));
			auto match = re.match(data);
			if (match.capturedLength("lt") && match.captured("lt") == "T")
			{
				setError(start, QString("FS-Code not supported: '%1%2'").arg(code).arg(data));
				return false;
			}
			if (match.capturedLength("ai") && match.captured("ai") == "I")
			{
				setError(start, QString("FS-Code not supported: '%1%2'").arg(code).arg(data));
				return false;
			}
			if (!match.capturedLength("spec"))
			{
				setError(start, QString("Malformed FS-Code: '%1%2'").arg(code).arg(data));
				return false;
			}
			if (match.captured("ix") != match.captured("iy") ||
			        match.captured("fx") != match.captured("fy"))
			{
				setError(0, QString("X/Y mismatch: '%1%2'").arg(code).arg(data));
				return false;
			}

			m_context.coordinateIntegralDigits = match.captured("ix").toInt();
			m_context.coordinateFractionalDigits = match.captured("fx").toInt();
			m_context.coordinateNotation = AbsoluteCoordinateNotation;
			m_context.zeroBehaviour = LeadingZeroOmmited;
			m_context.update();
			m_codeStats.FS++;
			// TODO: add command to tell the processor about the precision (grid) of the design
			return true;
		}
		else if (code == "MO")
		{
			static const QRegularExpression re(QString("^(IN|MM)\\*$"));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(0, QString("Malformed MO-Code: '%1%2'").arg(code).arg(data));
				return false;
			}
			SelectUnitCommand *cmd;
			if (match.captured(1) == "IN")
				cmd = new SelectUnitCommand(ImperialUnit);
			else
				cmd = new SelectUnitCommand(MetricUnit);
			m_commands.append(cmd);
			m_codeStats.MO++;
			return true;
		}
		else if (code == "AD")
		{
			// Non standard: ADD075 (aperture number starts with a zero)
			// Non standard: ADD10C,2.3622e-006 (use sci notation)
			// Non standard: ADD13VB_Custom_Obstruct Trace/Via Round 5.0 Both, ... (Aperture name contains ' ' and '/')
			// Non standard: ADD73R, 0.0950 X0.1250* (PCAD: extra spaces
			// TODO: Allow these, but print a warning
			static const QRegularExpression re(QString("^D0?(?<number>%1)(?<name>%2)(?<list>,(?<modifiers> *%3 *(X *%3 *)*))?\\*$")
			                                   .arg(ParserPrivate::APERTURENUMBER)
			                                   .arg(ParserPrivate::NAME_RELAXED)
			                                   .arg(ParserPrivate::DECIMAL));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(0, QString("Malformed AD-Code: '%1%2'").arg(code).arg(data));
				return false;
			}

			QList<qreal> parameters;
			if (match.capturedLength("list"))
				for (const QString &modifier: match.captured("modifiers").split(QChar('X')))
					parameters.append(modifier.trimmed().toDouble());

			auto cmd = new AddApertureCommand(match.captured("number").toInt(),
			                                  match.captured("name"),
			                                  parameters);
			m_commands.append(cmd);
			m_codeStats.AD++;
			return true;
		}
		else if (code == "AM")
		{
			QList<AperturePrimitive> aperturePrimitives;
			QString apertureName;

			QStringList primitives = data.split(QChar('*'));
			if (primitives.count() < 1)
			{
				setError(start, QString("Malformed AM-code: '%1%2'").arg(code).arg(data));
				return false;
			}

			apertureName = primitives.at(0);

			for (int p = 1; p<primitives.count(); p++)
			{
				auto primitive = primitives.at(p);

				if (primitive.isEmpty())
					continue;

				if (primitive.startsWith(QChar('0')))
					continue; // comment

				QStringList modifiers = primitive.split(QChar(','));

				if (modifiers.count() < 2) // type + exposure
				{
					setError(start, QString("Malformed primitive in AM-code: '%1%2'").arg(code).arg(data));
					return false;
				}

				for (auto modifier: modifiers)
				{
					auto str = modifier.trimmed();
					if (str.isEmpty())
					{
						setError(start, QString("Malformed primitive modifiers in AM-code: '%1%2'").arg(code).arg(data));
						return false;
					}
				}

				bool ok;

				int primitiveType = modifiers.at(0).toInt(&ok);
				if (!ok)
				{
					setError(start, QString("Malformed primitive type in AM-code: '%1%2'").arg(primitiveType).arg(data));
					return false;
				}

				bool primitiveExposure = true;
				int firstModifier = 1;
				if (primitiveType != 7 && primitiveType != 6) // Thermal and Moiré always with exposure on
				{
					firstModifier = 2;
					int tmpExposure = modifiers.at(1).toInt(&ok);
					if (!ok || (tmpExposure != 0 && tmpExposure != 1))
					{
						setError(start, QString("Malformed primitive exposure in AM-code: '%1%2'").arg(primitiveType).arg(data));
						return false;
					}
					if (tmpExposure == 0)
						primitiveExposure = false;
				}
				QList<qreal> primitiveModifiers;
				for (int i = firstModifier; i < modifiers.count(); i++)
				{
					primitiveModifiers.append(modifiers.at(i).toFloat(&ok));
					if (!ok)
					{
						setError(start, QString("Malformed primitive modifiers in AM-code: '%1%2'").arg(primitiveType).arg(data));
						return false;
					}
				}
				aperturePrimitives.append(AperturePrimitive(primitiveType, primitiveExposure, primitiveModifiers));
			}
			m_commands.append(new AddApertureTemplateCommand(apertureName, aperturePrimitives));
			m_codeStats.AM++;
			return true;
		}
		else if (code == "LP")
		{
			static const QRegularExpression re(QString("^(C|D)\\*$"));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(0, QString("Malformed LP-Code: '%1%2'").arg(code).arg(data));
				return false;
			}

			Polarity polarity = InvalidPolarity;
			if (match.captured(1) == "C")
				polarity = ClearPolarity;
			else if (match.captured(1) == "D")
				polarity = DarkPolarity;

			auto cmd = new SelectPolarityCommand(polarity);
			m_commands.append(cmd);
			m_codeStats.LP++;
			return true;
		}
		else if (code == "SR")
		{
			setError(start, QString("Unsupported repeat command: '%1%2'").arg(code).arg(data));
			m_codeStats.SR++;
			return false;
		}
		// FIXME: TF: be more relaxed with FIELD:
		else if (code == "TF" || code == "TA")
		{
			static const QRegularExpression re(QString("^(?<name>%1)(?<value>(,%2)*)\\*$")
			                                   .arg(ParserPrivate::NAME)
			                                   .arg(ParserPrivate::FIELD_RELAXED));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(start, QString("Malformed TF/TD/TA-Code: '%1%2'").arg(code).arg(data));
				return false;
			}
			auto name = match.captured("name");
			auto values = match.captured("value").split(QChar(','));
			if (!values.isEmpty())
				values.removeFirst(); // First always empty, since regexp starts with ','

			if (code == "TF")
			{
				auto cmd = new SetFileAttributeCommand;
				cmd->name = name;
				cmd->values = values;
				m_commands.append(cmd);
				m_codeStats.TF++;
			}
			else
			{
				auto cmd = new SetApertureAttributeCommand;
				cmd->name = name;
				cmd->values = values;
				m_commands.append(cmd);
				m_codeStats.TA++;
			}
			return true;
		}
		else if (code == "TD")
		{
			static const QRegularExpression re(QString("^(?<name>%1)\\*$")
			                                   .arg(ParserPrivate::NAME));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(start, QString("Malformed TF/TD/TA-Code: '%1%2'").arg(code).arg(data));
				return false;
			}
			auto cmd = new ClearApertureAttributeCommand;
			cmd->name = match.captured("name");
			m_commands.append(cmd);
			m_codeStats.TD++;
			return true;
		}
		else if (code == "AS")
		{
			if (data != "AXBY*")
			{
				setError(start, QString("Unsupported axis select command: '%1%2'").arg(code).arg(data));
				return false;
			}
			m_codeStats.AS++;
			return true;
		}
		else if (code == "IN")
		{
			// FIXME: this should be equivalent to a standard file attribute
			addWarning(start, QString("Ignoring deprecated IN-Code: '%1%2'").arg(code).arg(data));
			m_codeStats.IN++;
			return true;
		}
		else if (code == "IP")
		{
			// FIXME: this should be equivalent to a standard file attribute
			static const QRegularExpression re(QString("^(POS|NEG)\\*$"));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(0, QString("Malformed LP-Code: '%1%2'").arg(code).arg(data));
				return false;
			}

			if (match.captured(1) == "NEG")
			{
				setError(start, QString("Unsupported image polarity command: '%1%2'").arg(code).arg(data));
				return false;
			}

			m_codeStats.IP++;
			return true;
		}
		else if (code == "IR")
		{
			setError(start, QString("Unsupported image rotation command: '%1%2'").arg(code).arg(data));
			m_codeStats.IR++;
			return false;
		}
		else if (code == "LN")
		{
			m_commands.append(new CommentCommand(QStringLiteral("LN"), QString(data)));
			m_codeStats.LN++;
			return true;
		}
		else if (code == "MI")
		{
			setError(start, QString("Unsupported image mirroring command: '%1%2'").arg(code).arg(data));
			m_codeStats.MI++;
			return false;
		}
		else if (code == "OF")
		{
			static const QRegularExpression re(QString("^A(?<a>%1)B(?<b>%1)\\*$").arg(ParserPrivate::DECIMAL));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(0, QString("Malformed OF-Code: '%1%2'").arg(code).arg(data));
				return false;
			}

			qreal a = match.captured("a").toFloat();
			qreal b = match.captured("b").toFloat();
			if (!qFuzzyCompare(a, 0.0) || !qFuzzyCompare(b, 0.0))
			{
				setError(start, QString("Unsupported image offset command: '%1%2'").arg(code).arg(data));
				return false;
			}

			m_codeStats.OF++;
			return true;
		}
		else if (code == "SF")
		{
			static const QRegularExpression re(QString("^A(?<a>%1)B(?<b>%1)\\*$").arg(ParserPrivate::DECIMAL));
			auto match = re.match(data);
			if (!match.hasMatch())
			{
				setError(0, QString("Malformed SF-Code: '%1%2'").arg(code).arg(data));
				return false;
			}

			qreal a = match.captured("a").toFloat();
			qreal b = match.captured("b").toFloat();
			if (!qFuzzyCompare(a, 1.0) || !qFuzzyCompare(b, 1.0))
			{
				setError(start, QString("Unsupported scaling command: '%1%2'").arg(code).arg(data));
				return false;
			}
			m_codeStats.SF++;
			return true;
		}
		else
		{
			return false;
		}
	}

	bool FileReader::handleGarbage(int start, int length)
	{
		if (isValid())
		{
			QString garbage = m_data.mid(start, qMin(50, length));
			garbage.replace("\r", "\\r")
			        .replace("\n", "\\n");
			if (length > 50)
				garbage.append("[...]");
			setError(start, QStringLiteral("Unhandle data: '%1'").arg(garbage));
		}
		return false;
	}

	bool FileReader::isValid() const
	{
		return m_error.first == -1;
	}

	QString FileReader::errorString() const
	{
		return m_error.second;
	}

	int FileReader::errorLocation() const
	{
		return m_error.first;
	}

	int FileReader::warningCount() const
	{
		return m_warnings.count();
	}

	QStringList FileReader::warnings() const
	{
		return m_warnings.values();
	}

	int FileReader::codeCount() const
	{
		return m_codeStats.totalCount();
	}

	QList<Command *> FileReader::takeCommands()
	{
		QList<Command *> result;
		m_commands.swap(result);
		return result;
	}

	GerberCodeStatistics FileReader::codeStatistics() const
	{
		return m_codeStats;
	}

	void FileReader::clearErrorAndWarnings()
	{
		m_error.first = -1;
		m_error.second = QStringLiteral("No error");
		m_warnings.clear();
	}

	void FileReader::setError(int offset, const QString &message)
	{
		m_error.first = offset;
		m_error.second = message;
	}

	void FileReader::addWarning(int offset, const QString &message)
	{
		m_warnings.insertMulti(offset, message);
	}

	void FileReader::clearCommands()
	{
		qDeleteAll(m_commands);
		m_commands.clear();
	}

	void FileReader::clearContext()
	{
		m_context.clear();
		m_context.update();
	}

	void FileReader::clearStatements()
	{
		m_statements.clear();
	}

	void FileReader::clearStatisitics()
	{
		m_codeStats = GerberCodeStatistics();
	}

	int FileReader::commandCount() const
	{
		return m_commands.count();
	}

	CoordinateNotation FileReader::coordinateNotation() const
	{
		return m_context.coordinateNotation;
	}

	ZeroBehaviour FileReader::zeroBehaviour() const
	{
		return m_context.zeroBehaviour;
	}

	int FileReader::coordinateIntegralDigits() const
	{
		return m_context.coordinateIntegralDigits;
	}

	int FileReader::coordinateFractionalDigits() const
	{
		return m_context.coordinateFractionalDigits;
	}

	QPointF FileReaderContext::readCoordinate(const QString &x, const QString &y) const
	{
		return QPointF(readCoordinateNumber(x), readCoordinateNumber(y));
	}

	qreal FileReaderContext::readCoordinateNumber(const QString &str) const
	{
		if (str.isEmpty())
			return qSNaN();

		bool ok;
		int value = str.toInt(&ok);
		if (!ok)
			return qSNaN();
		return  value * qPow(10.0, -coordinateFractionalDigits);
	}

	void FileReaderContext::clear()
	{
		coordinateNotation = InvalidCoordinateNotation;
		zeroBehaviour = InvalidZeroBehaviour;
		coordinateIntegralDigits = 2;
		coordinateFractionalDigits = 5;
	}

}